<template xmlns:v-click-outside="http://www.w3.org/1999/xhtml">
  <div
    :class="wrapperClasses"
    v-click-outside:[capture].mousedown="handleClose"
    v-click-outside:[capture].touchstart="handleClose"
    v-click-outside:[capture]="handleClose"
  >
    <div ref="reference" :class="[prefixCls + '-rel']">
      <slot>
        <e-input
          :key="forceInputRerender"
          :element-id="elementId"
          :class="[prefixCls + '-editor']"
          :readonly="!editable || readonly"
          :disabled="itemDisabled"
          :placeholder="placeholder"
          :model-value="visualValue"
          :name="name"
          ref="input"

          @on-input-change="handleInputChange"
          @on-focus="handleFocus"
          @on-blur="handleBlur"
          @click="handleFocus"
          @keydown="handleKeydown"
          @mouseenter="handleInputMouseenter"
          @mouseleave="handleInputMouseleave"
        >
          <template #suffix>
            <Icon @click="handleIconClick" :type="arrowType" :class="[prefixCls + '-icon']" />
          </template>
        </e-input>
      </slot>
    </div>
    <Drop
      ref="drop"
      :visible="opened"
      :classes="dropdownCls"
      :eventsEnabled="eventsEnabled"
      :transfer="transfer"
      transition-name="transition-drop"
      @click="handleTransferClick"
    >
      <div>
        <component
          :is="panel"
          ref="pickerPanel"
          :visible="visible"
          :separator="separator"
          :steps="steps"
          :format="format"
          :model-value="internalValue"
          :visual-value="visualValue"
          :picker-type="type"
          v-bind="ownPickerProps"
          @on-pick="onPick"
          @on-pick-click="disableClickOutSide = true"
        ></component>
      </div>
    </Drop>
  </div>
</template>

<script>
import { getCurrentInstance, nextTick } from 'vue';
import { directive as clickOutside } from '../../directives/v-click-outside-x';
import mixinsForm from '../../mixins/form';
import { oneOf } from '../../utils/assist';
import Icon from '../../components/icon/icon.vue';
import Drop from '../../components/select/dropdown.vue';
import TimePickerPanel from './components/time.vue';
import RangeTimePickerPanel from './components/time-range.vue';
import { formatDate, DEFAULT_FORMATS, TYPE_VALUE_RESOLVER_MAP, getDayCountOfMonth } from './config/util';
const prefixCls = 'haloe-time-picker';
const isEmptyArray = val => val.reduce((isEmpty, str) => isEmpty && !str || (typeof str === 'string' && str.trim() === ''), true);
const extractTime = date => {
  if (!date) return [0, 0, 0];
  return [
    date.getHours(), date.getMinutes(), date.getSeconds()
  ];
};
export default {
  name: "picker",
  directives: { clickOutside },
  mixins: [ mixinsForm ],
  provide () {
    return {
      PickerInstance: this
    }
  },
  emits: ['on-clickoutside', 'on-clear', 'on-change', 'on-ok', 'on-open-change', 'update:modelValue'],
  components: { Drop, Icon, TimePickerPanel, RangeTimePickerPanel },
  data() {
    const isRange = this.type.includes('range');
    const emptyArray = isRange ? [null, null] : [null];
    const initialValue = isEmptyArray((this.type.includes('range') ? this.modelValue : [this.modelValue]) || []) ? emptyArray : this.parseDate(this.modelValue);
    const focusedTime = initialValue.map(extractTime);
    return {
      showClose: false,
      prefixCls: prefixCls,
      forceInputRerender: 1,
      internalValue: initialValue,
      visible: false,
      isFocused: false,
      disableClickOutSide: false,    // fixed when click a date,trigger clickoutside to close picker
      disableCloseUnderTransfer: false,  // transfer 模式下，点击Drop也会触发关闭,
      focusedTime: {
        column: 0, // which column inside the picker
        picker: 0, // which picker
        time: focusedTime, // the values array into [hh, mm, ss],
        active: false
      },
      internalFocus: false,
    }
  },
  props: {
    disabledHours: {
      type: Array,
      default () {
        return [];
      }
    },
    disabledMinutes: {
      type: Array,
      default () {
        return [];
      }
    },
    disabledSeconds: {
      type: Array,
      default () {
        return [];
      }
    },
    format: {
      type: String
    },
    steps: {
      type: Array,
      default: () => []
    },
    separator: {
      type: String,
      default: ' - '
    },
    type: {
      validator (value) {
        return oneOf(value, ['time', 'timerange']);
      },
      default: 'time'
    },
    open: {
      type: Boolean,
      default: null
    },
    disabled: {
      type: Boolean,
      default: false
    },
    modelValue: {
      type: [Date, String, Array],
    },
    name: {
      type: String
    },
    elementId: {
      type: String
    },
    readonly: {
      type: Boolean,
      default: false
    },
    editable: {
      type: Boolean,
      default: true
    },
    placeholder: {
      type: String,
      default: ''
    },
    hideDisabledOptions: {
      type: Boolean,
      default: false
    },
    transferClassName: {
      type: String
    },
    options: {
      type: Object,
      default: () => ({})
    },
    clearable: {
      type: Boolean,
      default: true
    },
    // 4.0.0
    capture: {
      type: Boolean,
      default () {
        const global = getCurrentInstance().appContext.config.globalProperties;
        return !global.$VIEWUI ? true : global.$VIEWUI.capture;
      }
    },
    // 4.6.0
    eventsEnabled: {
      type: Boolean,
      default: false
    },
    transfer: {
      type: Boolean,
      default () {
        const global = getCurrentInstance().appContext.config.globalProperties;
        return !global.$VIEWUI || global.$VIEWUI.transfer === '' ? false : global.$VIEWUI.transfer;
      }
    }
  },
  computed: {
    panel () {
      return this.type === 'timerange' ? 'RangeTimePickerPanel': 'TimePickerPanel';
    },
    ownPickerProps () {
      return {
        disabledHours: this.disabledHours,
        disabledMinutes: this.disabledMinutes,
        disabledSeconds: this.disabledSeconds,
        hideDisabledOptions: this.hideDisabledOptions
      };
    },
    opened () {
      return this.open === null ? this.visible : this.open;
    },
    dropdownCls () {
      return {
        [prefixCls + '-transfer']: this.transfer,
        [this.transferClassName]: this.transferClassName
      };
    },
    visualValue() {
      return this.formatDate(this.internalValue);
    },
    wrapperClasses() {
      return [prefixCls, {
        [prefixCls + '-focused']: this.isFocused
      }];
    },
    arrowType () {
      if (!this.showClose) {
        return 'clock';
      }
      return 'clear';
    },
    publicStringValue (){
      const isRange = this.type.includes('range');
      let val = this.internalValue.map(date => date instanceof Date ? new Date(date) : (date || ''));
      return isRange ? val : val[0];
    },
    transition () {
      const bottomPlaced = this.placement.match(/^bottom/);
      return bottomPlaced ? 'slide-up' : 'slide-down';
    },
  },
  methods: {
    parseDate(val) {
      const { type } = this;
      const isRange = type.includes('range');
      const parser = (
        TYPE_VALUE_RESOLVER_MAP[type] ||
        TYPE_VALUE_RESOLVER_MAP['default']
      ).parser;
      const format = this.format || DEFAULT_FORMATS[type];

      if (val && type === 'time' && !(val instanceof Date)) {
        val = parser(val, format, this.separator);
      } else if (isRange) {
        val = parser(val, format, this.separator).map(v => v || '');
      }
      return isRange ? (val || []) : [val];
    },
    formatDate(value){
      const format = DEFAULT_FORMATS[this.type];
      const {formatter} = (
        TYPE_VALUE_RESOLVER_MAP[this.type] ||
        TYPE_VALUE_RESOLVER_MAP['default']
      );
      return formatter(value, this.format || format, this.separator);
    },
    onPickSuccess(){
      this.visible = false;
      this.$emit('on-ok');
      this.focus();
      this.reset();
    },
    focus() {
      this.$refs.input && this.$refs.input.focus();
    },
    reset(){
      this.$refs.pickerPanel.reset && this.$refs.pickerPanel.reset();
    },
    handleClear () {
      this.visible = false;
      this.internalValue = this.internalValue.map(() => null);
      this.$emit('on-clear');
      this.emitChange(this.type);
      this.reset();
    },
    emitChange (type) {
      nextTick(() => {
        this.$emit('on-change', this.publicStringValue, type);
        this.handleFormItemChange('change', this.publicStringValue);
      });
    },
    onPick(dates, type) {
      dates = this.parseDate(dates);
      this.internalValue = Array.isArray(dates) ? dates : [dates];
      this.focusedTime = {
        ...this.focusedTime,
        time: this.internalValue.map(extractTime)
      };
      this.emitChange(type);
    },
    handleInputMouseleave () {
      this.showClose = false;
    },
    // 开启 transfer 时，点击 Drop 即会关闭，这里不让其关闭
    handleTransferClick () {
      if (this.transfer) this.disableCloseUnderTransfer = true;
    },
    handleInputMouseenter () {
      if (this.readonly || this.itemDisabled) return;
      if (this.visualValue && this.clearable) {
        this.showClose = true;
      }
    },
    handleIconClick (e) {
      if (this.showClose) {
        if (e) e.stopPropagation();
        this.handleClear();
      } else if (!this.itemDisabled) {
        this.handleFocus();
      }
    },
    handleKeydown(e){
      const keyCode = e.keyCode;

      // handle "tab" key
      if (keyCode === 9){
        if (this.visible){
          e.stopPropagation();
          e.preventDefault();

          this.handleClose();
        } else {
          this.focused = false;
        }
      }

      // open the panel
      const arrows = [37, 38, 39, 40];
      if (!this.visible && arrows.includes(keyCode)){
        this.visible = true;
        return;
      }

      // close on "esc" key
      if (keyCode === 27){
        if (this.visible) {
          e.stopPropagation();
          this.handleClose();
        }
      }

      // select date, "Enter" key
      if (keyCode === 13){
        const timePickers = this.timeSpinnerList.map(item => item.timeSpinner);
        if (timePickers.length > 0){
          const columnsPerPicker = timePickers[0].showSeconds ? 3 : 2;
          const pickerIndex = Math.floor(this.focusedTime.column / columnsPerPicker);
          const value = this.focusedTime.time[pickerIndex];

          timePickers[pickerIndex].chooseValue(value);
          return;
        }

        if (this.type.match(/range/)){
          this.$refs.pickerPanel.handleRangePick(this.focusedDate, 'date');
        } else {
          const panels = this.panelTableList.map(item => item.panelTable);
          const compareDate = (d) => {
            const sliceIndex = ['year', 'month', 'date'].indexOf((this.type)) + 1;
            return [d.getFullYear(), d.getMonth(), d.getDate()].slice(0, sliceIndex).join('-');
          };
          const dateIsValid = panels.find(({cells}) => {
            return cells.find(({date, disabled}) => compareDate(date) === compareDate(this.focusedDate) && !disabled);
          });
          if (dateIsValid) this.onPick(this.focusedDate, false, 'date');
        }
      }

      if (!arrows.includes(keyCode)) return; // ignore rest of keys

      // navigate times and dates
      if (this.focusedTime.active) e.preventDefault(); // to prevent cursor from moving
      this.navigateDatePanel(keyValueMapper[keyCode], e.shiftKey);
    },
    handleBlur (e) {
      if (this.internalFocus){
        this.internalFocus = false;
        return;
      }
      if (this.visible) {
        e.preventDefault();
        return;
      }

      this.isFocused = false;
      this.internalValue = this.internalValue.slice(); // trigger panel watchers to reset views
      this.reset();
      this.$refs.pickerPanel.onToggleVisibility(false);
    },
    handleFocus (e) {
      if (this.readonly || this.itemDisabled) return;
      this.isFocused = true;
      if (e && e.type === 'focus') return; // just focus, don't open yet
      if(!this.itemDisabled){
        this.visible = true;
      }
    },
    handleInputChange (event) {
      const isArrayValue = this.type.includes('range');
      const oldValue = this.visualValue;
      const newDate = event.target.value;
      const disabledDateFn =
        this.options &&
        typeof this.options.disabledDate === 'function' &&
        this.options.disabledDate;
      const valueToTest = isArrayValue ? newDate : newDate[0];
      const isDisabled = disabledDateFn && disabledDateFn(valueToTest);
      const isValidDate = newDate.reduce((valid, date) => valid && date instanceof Date, true);

      if (newDate !== oldValue && !isDisabled && isValidDate) {
        this.emitChange(this.type);
        this.internalValue = newDate;
      } else {
        this.forceInputRerender++;
      }
    },
    handleClose (e) {
      if (this.disableCloseUnderTransfer) {
        this.disableCloseUnderTransfer = false;
        return false;
      }

      if (e && e.type === 'mousedown' && this.visible) {
        e.preventDefault();
        e.stopPropagation();
        return;
      }

      if (this.visible) {
        const pickerPanel = this.$refs.pickerPanel && this.$refs.pickerPanel.$el;
        if (e && pickerPanel && pickerPanel.contains(e.target)) return; // its a click inside own component, lets ignore it.

        this.visible = false;
        e && e.preventDefault();
        e && e.stopPropagation();
        this.$emit('on-clickoutside', e);
        return;
      }

      this.isFocused = false;
      this.disableClickOutSide = false;
    },
  }
}
</script>

<style scoped>

</style>
